/*
 * Copyright (c) 2009, Jan Pastor. All Rights Reserved.
 */

package sk.jp.jitag.dao.db4o;

import java.util.Collection;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.db4o.ObjectSet;
import com.db4o.ext.ExtObjectContainer;
import com.db4o.query.Predicate;

import sk.jp.commons.exception.ApplicationException;
import sk.jp.commons.utils.ExceptionUtils;
import sk.jp.commons.utils.LogMsg;
import sk.jp.jitag.dao.DataSource;
import sk.jp.jitag.dao.DataSourceProvider;
import sk.jp.jitag.dao.JitagDao;
import sk.jp.jitag.model.File;
import sk.jp.jitag.model.config.Config;

@Component
public class Db4oJitagDao implements JitagDao {

    private static final Logger log = LoggerFactory.getLogger(Db4oJitagDao.class);

    @Autowired
    private DataSourceProvider dataSourceProvider;

    private ThreadLocal<Db4oDataSource> threadLocalDataSource = new ThreadLocal<Db4oDataSource>();

    //----------------------------------------------------------------------

    /**
     * If called in existing transaction, does nothing.
     */
    public void beginTransaction() {
        if(threadLocalDataSource.get() == null){
            DataSource dataSource = dataSourceProvider.getDataSource();
            threadLocalDataSource.set((Db4oDataSource) dataSource);
        }
    }

    public void commitTransaction() {
        checkIsInTransaction();
        ExtObjectContainer dataSource = threadLocalDataSource.get().getDb4oDataSource();
        dataSource.commit();
    }

    public void rollbackTransaction(Throwable cause) {
        log.info(LogMsg.inp("cause", cause.getMessage()), cause);
        checkIsInTransaction();
        ExtObjectContainer dataSource = threadLocalDataSource.get().getDb4oDataSource();
        dataSource.rollback();
    }

    public void doInTransaction(Runnable task) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp());
        }
        if(task == null) throw new NullPointerException("Parameter task is null");

        beginTransaction();
        try {
            task.run();
            commitTransaction();
            if(log.isTraceEnabled()){
                log.trace("Transaction commited");
            }
        }catch(Throwable e){
            rollbackTransaction(e);
            log.trace("Transaction rollbacked (cause: "+e+")");
            throw new TransactionRolledbackException("An error occurred during operation, transaction was rollbacked",
                                                     "sk.jp.jitag.dao.db4o.Db4oJitagDao.err.001",
                                         null, e);
        }finally{
            close();
        }
    }

    /**
     * Doesn't throws exception. If already closed, or not initialized before,
     * does nothing.
     */
    public void close() {
        log.trace("Closing database client connection...");
        if(threadLocalDataSource.get() == null){
            log.debug("Database client connection already closed or not initialized before");
            return;
        }
        try {
            ExtObjectContainer dataSource = threadLocalDataSource.get().getDb4oDataSource();
            dataSource.close();
            threadLocalDataSource.remove();
            log.debug("Database client connection closed.");
        } catch (Throwable ignored) {
            log.warn("Can't close database client connection");
        }
    }

    //----------------------------------------------------------------------

    public void markFilesForDelete(final Collection<String> fileExtensions) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("fileExtensions", fileExtensions));
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();

        ObjectSet<File> files = db.query(new Predicate<File>() {
                                    @Override
                                    public boolean match(File candidate) {
                                        return candidate.getExtension() != null
                                               && fileExtensions.contains(
                                                    candidate.getExtension().toLowerCase());
                                    }
                                });
        for (File file : files) {
            db.store(file.setForDelete(true));
        }
    }

    public FileSyncState isChanged(Integer fileId, Integer fileHash) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("fileId", fileId, "fileHash", fileHash));
        }
        checkIsInTransaction();
        File file = getFile(fileId);
        FileSyncState fileSyncState;
        if(file == null){
            fileSyncState = FileSyncState.missing;
        }else if(file.getHash().equals(fileHash)){
            fileSyncState = FileSyncState.equals;
        }else{
            fileSyncState = FileSyncState.changed;
        }

        if(log.isTraceEnabled()){
            log.trace(LogMsg.out("fileSyncState", fileSyncState));
        }
        return fileSyncState;
    }

    public int deleteMarkedFiles() {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp());
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        ObjectSet<File> filesToDelete = db.query(new Predicate<File>() {
                                            @Override
                                            public boolean match(File _file) {
                                                return _file.isForDelete();
                                            }
                                        });
        for (File file : filesToDelete) {
            db.delete(file);
        }
        return filesToDelete.size();
    }

    public void unmarkFileForDeleteFlag(Integer fileId) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("fileId", fileId));
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        File file = getFile(fileId);
        if(file != null){
            db.store(file.setForDelete(false));
        }
    }

    public void saveNewFile(File file) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("file", file));
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        db.store(file);
    }

    public void updateChangedFile(File file) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("file", file));
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        db.store(file);
    }

    public <T extends BaseEnum> T get(String code, Class<T> clazz) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("code", code, "entityClass", clazz.getSimpleName()));
        }
        checkIsInTransaction();

        T enumItem = getOptional(code, clazz);
        if(enumItem == null){
            throw new ApplicationException("Enumeration item not was found",
                           null,  //TODO Dokoncit!!! code
                           ExceptionUtils.paramsToMap("enumerationClass", clazz.getSimpleName(),
                                                      "code", code));
        }

        if(log.isTraceEnabled()){
            log.trace(LogMsg.out("enumItem", enumItem));
        }
        return enumItem;
    }

    public <T extends BaseEnum> T getOptional(final String code, final Class<T> clazz) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("code", code, "entityClass", clazz.getSimpleName()));
        }
        if(code == null) throw new NullPointerException("Parameter code is null");
        if(clazz == null) throw new NullPointerException("Parameter clazz is null");

        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        ObjectSet<T> items = db.query(new Predicate<T>() {
                                    @Override
                                    public boolean match(T _enum) {
                                        return _enum.getClass().equals(clazz)
                                               && _enum.getCode().equals(code);
                                    }
                                });
        T enumItem = Db4oUtils.getOne(items);

        if(log.isTraceEnabled()){
            log.trace(LogMsg.out("enumItem", enumItem));
        }
        return enumItem;
    }

    public <T extends BaseEnum> T getOrNew(String code, Class<T> clazz, T enumInstance) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("code", code, "entityClass", clazz.getSimpleName(),
                                 "enumInstance", enumInstance));
        }
        checkIsInTransaction();

        T enumItem = getOptional(code, clazz);
        if(enumItem == null){
            ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
            db.store(enumInstance);
            enumItem = enumInstance;
        }

        if(log.isTraceEnabled()){
            log.trace(LogMsg.out("enumItem", enumItem));
        }
        return enumItem;
    }

    public <T extends BaseEntity> List<T> getAll(final Class<T> clazz) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("clazz", clazz.getSimpleName()));
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        ObjectSet<T> items = db.query(new Predicate<T>() {
                                    @Override
                                    public boolean match(T _entity) {
                                        return _entity.getClass().equals(clazz);
                                    }
                                });
        if(log.isTraceEnabled()){
            log.trace(LogMsg.out("items", items));
        }
        return items;
    }

    public File getFile(final Integer fileId) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("fileId", fileId));
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        ObjectSet<File> files = db.query(new Predicate<File>() {
                                        @Override
                                        public boolean match(File _file) {
                                            return _file.getId().equals(fileId);
                                        }
                                    });
        File file = Db4oUtils.getOne(files);
        if(log.isTraceEnabled()){
            log.trace(LogMsg.out("file", file));
        }
        return file;
    }

    public Config getConfig() {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp());
        }
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();

        Config config = Db4oUtils.getOne(db.query(Config.class));
        if(log.isTraceEnabled()){
            log.trace(LogMsg.out("config", config));
        }
        return config;
    }

    public void saveConfig(final Config config) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.inp("config", config));
        }
        checkIsInTransaction();

        final ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        Config configInDb = Db4oUtils.getOne(db.query(Config.class));
        if(configInDb != null){
            db.delete(configInDb); //remove old - complete config tree. Unreferenced items no remaind alter.
        }
        db.store(config); //save as clearly new.
    }

    public void copyDatabaseFile(String newDbPath) {
        if(log.isDebugEnabled()){
            log.debug(LogMsg.out("newDbPath", newDbPath));
        }
        checkIsInTransaction();
        dataSourceProvider.backup(newDbPath);
    }

    /**
     * Dumps all persisted instances of specified entity to log.
     * <p>For testing purposes ONLY.
     * @param db
     * @param entityClass
     * @param <T>
     */
    <T extends BaseEntity> void listEntities(Class<T> entityClass){
        checkIsInTransaction();
        ExtObjectContainer db = threadLocalDataSource.get().getDb4oDataSource();
        Db4oUtils.listEntities(db, entityClass);
    }

    /**
     * Returns datasource handle.
     * <p>For testing purposes ONLY.
     * @return db4o client connection.
     */
    ExtObjectContainer getDataSource(){
        beginTransaction();
        return threadLocalDataSource.get().getDb4oDataSource();
    }

    //----------------------------------------------------------------------

    /**
     * Checks if current thread runs a DAO method in transaction, ehich
     * means that client connection to server was initialized and DAO
     * is ready to work.
     * <p>This method must be called on begin of every public DAO method!
     */
    private void checkIsInTransaction(){
        if(threadLocalDataSource.get() == null){
            throw new ApplicationException("Transaction is not initialized",
                                           "sk.jp.jitag.dao.db4o.Db4oJitagDao.err.002", null); 
        }
    }

}
